
package cz.czu.pef.DataModelling.dao.impl;

import java.util.ArrayList;
import java.util.Collection;
import java.util.List;

import java.util.logging.Level;
import java.util.logging.Logger;

import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;

import org.hibernate.*;
import org.hibernate.criterion.*;
import org.springframework.beans.factory.annotation.Configurable;
import org.springframework.core.GenericTypeResolver;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
//import org.springframework.orm.hibernate3.support.HibernateDaoSupport;
//import org.springframework.transaction.annotation.Transactional;

import cz.czu.pef.DataModelling.dao.*;
import cz.czu.pef.DataModelling.entityObjects.IdPersistenceEntityObject;
import cz.czu.pef.common.CustomFilterField;
import cz.czu.pef.common.CustomSortField;

/**
 * Abstract class which implements shared basic DAO operations for given Entity.
 * 
 * @author Rostislav Stříbrný
 *
 * @param <T>
 */
@Component
@Transactional
@Configurable
public class AbstractDaoHibernateImpl<T extends IdPersistenceEntityObject> implements Dao<T>
{

	private static final Logger logger = Logger.getLogger(AbstractDaoHibernateImpl.class.getName());

  private final Class<T> genericType;

  @SuppressWarnings("unchecked")
  public AbstractDaoHibernateImpl()
  {
    this.genericType = (Class<T>) GenericTypeResolver.resolveTypeArgument(getClass(), AbstractDaoHibernateImpl.class);
  }

  protected Class<?> getEntityClass()
  {
    return genericType;
  }

	//@SuppressWarnings("unused")
	//private Session session;
	//public void setSession(Session s) {
	//	this.session = s;
	//}

	@PersistenceContext
	private EntityManager entityManager;

	public void setEntityManager(EntityManager entityManager)
	{
		this.entityManager = entityManager;
	}
	
	public EntityManager getEntityManager()
	{
		return this.entityManager;
	}
	
	private Session getHibernateSession()
	{
	  org.hibernate.Session session;
	  // http://stackoverflow.com/questions/4148231/how-can-i-get-the-session-object-if-i-have-the-entitymanager
	  //session = (Session) entityManager.getDelegate(); // JPA 1.0 spec style

	  // http://docs.jboss.org/hibernate/orm/4.1/devguide/en-US/html/ch03.html#d5e953
	  session = entityManager.unwrap(Session.class); // JPA 2.0 spec style
	  return session;
	}

	/*
    private SessionImplementor getHibernateSessionImplementor()
    {
      return entityManager.unwrap( SessionImplementor.class);
    }
    */

	/*
	public AbstractDaoHibernateImpl<T>(final SessionFactory sessionFactory)
	{
	  setSessionFactory(sessionFactory);
	}
	*/

	@Override
	public void remove(T entity)
	{
		if (logger.isLoggable(Level.INFO))
		{
			logger.info("remove - going to delete entity: " + entity);
		}
		getHibernateSession().delete(entity);
	}

	@SuppressWarnings("unchecked")
    @Override
	public void removeById(long idValue)
	{
		if (logger.isLoggable(Level.INFO))
		{
			logger.info("removeById():: going to delete entity/entities with ID: " + idValue);
		}
		List<T> objsToDelete = getHibernateSession().createCriteria(getEntityClass()).add(Restrictions.eq("id", idValue)).list();
		for (T obj : objsToDelete)
		{
			logger.info("removeById():: going to delete entity: " + obj);
			remove(obj);
		}
		logger.info("removeById():: done");
	}

	@SuppressWarnings("unchecked")
    @Override
	public T getByStringId(String id)
	{
		if (logger.isLoggable(Level.INFO))
		{
			logger.info("getByStringId - getting entity by id (String): " + id);
		}
		return (T) getHibernateSession().get(getEntityClass(), Long.valueOf(id));
	}

	@SuppressWarnings("unchecked")
    @Override
	public T getById(long id)
	{
		if (logger.isLoggable(Level.INFO))
		{
			logger.info("getById - getting entity by id (long): " + id);
		}
		return (T) getHibernateSession().get(getEntityClass(), new Long(id));
	}

	@SuppressWarnings("unchecked")
    @Override
	public T getByIdLocked(long id)
	{
		if (logger.isLoggable(Level.INFO))
		{
			logger.info("getByIdLocked - getting locked entity by id: " + id);
		}
		return (T) getHibernateSession().get(getEntityClass(), id, LockOptions.UPGRADE);
	}

	@Override
	public void persist(T entity)
	{
		if (logger.isLoggable(Level.INFO))
		{
			logger.info("saveOrUpdate - going to save / update entity: " + entity);
		}
		getHibernateSession().saveOrUpdate(entity);
	}

	@SuppressWarnings("unchecked")
    @Override
	public T merge(T entity)
	{
		if (logger.isLoggable(Level.INFO))
		{
			logger.info("merge - going to merge entity: " + entity);
		}
		return (T)getHibernateSession().merge(entity);
	}

    @SuppressWarnings("unchecked")
    @Override
    public T mergeUnique(final String attribName, final Object value, T entity)
    {
        if (logger.isLoggable(Level.INFO))
        {
            logger.info("merge - going to merge entity: " + entity);
        }
        T uniqueEntity = findByAttributeUniqueResult(attribName, value);
        if (uniqueEntity != null)
        {
          // Copy unique ID so it will merge to the same object
          entity.setId(uniqueEntity.getId());
        }

        return (T)getHibernateSession().merge(entity);
    }

    @SuppressWarnings("unchecked")
    @Override
    public T mergeUniqueExt(final String[] attribNames, final Object[] values, T entity)
    {
        if (logger.isLoggable(Level.INFO))
        {
            logger.info("merge - going to merge entity: " + entity);
        }
        if (attribNames == null)
          throw new RuntimeException("attribNames is null");
        if (values == null)
          throw new RuntimeException("values is null");
        if (attribNames.length != values.length)
          throw new RuntimeException("attribNames.length != values.length");

        List<CustomFilterField> filterList = new ArrayList<CustomFilterField>();
        for (int i=0; i<attribNames.length; i++)
        {
          CustomFilterField cff = new CustomFilterField();
          cff.setPropertyName(attribNames[i]);
          cff.setFilterValue(values[i].toString());
          filterList.add(cff);
        }

        List<T> found = this.findAllByRangeWithFilter(null, null, filterList);
        if (found != null)
        {
          if (found.size() > 1)
            throw new RuntimeException("Found more than one record: " + found.size());

          // Copy unique ID so it will merge to the same object
          entity.setId(found.get(0).getId());
        }

        return (T)getHibernateSession().merge(entity);
    }

    @Override
	public Long count()
	{
		return countWithFilter(null);
	}

    @Override
	public Long countWithFilter(List<CustomFilterField> filterFields)
	{
		logger.info("countWithFilter - going to count the entities");
		Criteria crit = getHibernateSession().createCriteria(getEntityClass());
		addFilterCriterions(crit, filterFields);

		return (Long)crit.setProjection(Projections.rowCount()).uniqueResult();
	}

	/**
	 * Retrieves all entities of designated type
	 *
	 * @return
	 */
	@SuppressWarnings("unchecked")
    @Override
	public List<T> findAll()
	{
		logger.info("findAll():: Looking up for all entities");
		return getHibernateSession().createCriteria(getEntityClass()).list();
	}


	/**
	 * Retrieves all entities of designated type limited between firstRow and endRow
	 *
	 * @return
	 */
    @Override
	public List<T> findAllByRange(Integer firstRow, Integer endRow)
	{
		logger.info("findAllByRange():: Looking up for all entities between " + firstRow + " and " + endRow);
		return findAllByRangeWithFilterAndSort(firstRow, endRow, null, null);
	}

	/**
	 * Retrieves all entities of designated type limited ((between firstRow and endRow) AND (filterFields (if any)))
	 *
	 * @return
	 */
    @Override
	public List<T> findAllByRangeWithFilter(Integer firstRow, Integer endRow, List<CustomFilterField> filterFields)
	{
		logger.info("findAllByRangeWithFilter():: Looking up for all entities between " + firstRow + " and " + endRow);
		return findAllByRangeWithFilterAndSort(firstRow, endRow, filterFields, null);
	}
	
	private void addFilterCriterions(Criteria crit, List<CustomFilterField> filterFields)
	{
		if (filterFields != null)
		{
			for (CustomFilterField field: filterFields)
			{
				if (field != null && field.getPropertyName() != null && field.getFilterValue() != null)
					crit.add(Restrictions.ilike(field.getPropertyName(), field.getFilterValue()));
			}
		}
	}
	
	private void addSortCriterions(Criteria crit, List<CustomSortField> sortFields)
	{
		if (sortFields != null)
		{
			for (CustomSortField field: sortFields)
			{
				if (field != null && field.getPropertyName() != null)
				{
					if (CustomSortField.ASCENDING.equals(field.getOrdering()))
						crit.addOrder(Order.asc(field.getPropertyName()));
					if (CustomSortField.DESCENDING.equals(field.getOrdering()))
						crit.addOrder(Order.desc(field.getPropertyName()));
				}
			}
		}
	}

	/*
	 * Retrieves all entities of designated type limited ((between firstRow and endRow) AND (filterFields (if any))).
	 *     Ordered according to sortFields.
	 *
	 * @return
	 */
	@SuppressWarnings("unchecked")
    @Override
	public List<T> findAllByRangeWithFilterAndSort(Integer firstRow, Integer endRow, List<CustomFilterField> filterFields, List<CustomSortField> sortFields)
	{
		logger.info("findAllByRangeWithFilterAndSort():: Looking up for all entities between " + firstRow + " and " + endRow);
		Criteria crit = getHibernateSession().createCriteria(getEntityClass());

		if (firstRow != null && firstRow > 0)
		{
		  crit.setFirstResult(firstRow);
		  if (endRow != null && endRow > 0)
		    crit.setMaxResults(endRow - firstRow + 1);
		}

		addFilterCriterions(crit, filterFields);
		addSortCriterions(crit, sortFields);

		return crit.list();
	}

	/**
	 * Retrieves all entities of designated type filtered by condition
	 * attribName column == value
	 *
	 * @param attribName
	 * @param value
	 * @return
	 */
	@SuppressWarnings("unchecked")
    @Override
	public List<T> findByAttribute(final String attribName, final Object value)
	{
		if (logger.isLoggable(Level.INFO))
		{
			logger.info("findByAttribute - looking up for all entities where: " + attribName + " = " + value);
		}
		return getHibernateSession().createCriteria(getEntityClass()).add(Restrictions.eq(attribName, value)).list();
	}

	/**
	 * Retrieves one entity of designated type filtered by condition
	 * attribName column == value
	 *
	 * @param attribName
	 * @param value
	 * @return
	 */
	@SuppressWarnings("unchecked")
    @Override
	public T findByAttributeUniqueResult(final String attribName, final Object value)
	{
		if (logger.isLoggable(Level.INFO))
		{
			logger.info("findByAttribute - looking up for all entities where: " + attribName + " = " + value);
		}
		return (T)getHibernateSession().createCriteria(getEntityClass()).add(Restrictions.eq(attribName, value)).uniqueResult();
	}

	/**
	 * Retrieves all entities of designated type filtered by condition
	 * attribName column in values
	 *
	 * @param attribName
	 * @param value
	 * @return
	 */
	@SuppressWarnings("unchecked")
    @Override
	public List<T> findByAttributeIn(final String attribName, final Collection<? extends Object> value)
	{
		if (logger.isLoggable(Level.INFO))
		{
			logger.info("findByAttributeIn - looking up for all entities where: " + attribName + " in " + value);
		}
		return getHibernateSession().createCriteria(getEntityClass()).add(Restrictions.in(attribName, value)).list();
	}

	/**
	 * Create HQL query for usage by Daos (mainly mapping arguments etc...)
	 *
	 * @param hql
	 * @return Query object suitable for HQL queries
	 */
	protected Query createHqlQuery(final String hql)
	{
		if (logger.isLoggable(Level.INFO))
		{
			logger.info("createHqlQuery - going to create hql query: " + hql);
		}
		return getHibernateSession().createQuery(hql);
	}

	/**
	 * Create SQL query for usage by Daos (mainly mapping arguments etc...)
	 *
	 * @param sql
	 * @return Query object suitable for SQL queries
	 */
	protected SQLQuery createSqlQuery(final String sql)
	{
		if (logger.isLoggable(Level.INFO))
		{
			logger.info("createSqlQuery - going to create sql query: " + sql);
		}
		return getHibernateSession().createSQLQuery(sql);
	}

	/**
	 * Executes HQL or SQL query
	 *
	 * @param query
	 * @return List of entities, or arrays, as defined by HQL
	 */
	protected Object executeQuery(final Query query)
	{
		if (logger.isLoggable(Level.INFO))
		{
			logger.info("executeHql - executing HQL: " + query.getQueryString());
		}
		return query.list();
	}

}
